iT邦幫忙

2022 iThome 鐵人賽

DAY 18
0
自我挑戰組

我與 React 的 30天系列 第 18

Day 18 React.memo 聽說你很猛啊,useCallback 表示?

  • 分享至 

  • xImage
  •  

昨天提到了,React.memo 這個看似無敵以及堅強的人?竟然有個破綻?

memo 的弱點

我們將昨天的例子拿來用,並且將傳給 Parent.js 這個 component 的 props 改為傳遞一個物件下去,並且將的這個物件的 content 的value 印出來

// App.js
import { useState } from 'react';
import './index.css';
import { Parent } from './Parent';

function App() {
  const [count, setCount] = useState(0)
  console.log("APP 組建 渲染");

  const clickHandler = () => {
    setCount(perv => perv + 1)
  }

  return (
    <div className='border'>
      <div>
        我是在 APP.js 的 count :{ count }
      </div>
      <button onClick={clickHandler}>點我加一</button>
      <Parent item={ { content: "傳下來的東西"} } />
    </div>
  );
}

export default App;
// Parent.js
import './index.css'
import { Child } from "./Child"
import { memo } from 'react';

const Parent = memo(({ item }) => {
  console.log("我是Parent.js 為啥要渲染我啊");

  return (
    <div className='parent'>
      我是Parent.js
      <span className='item'>{ item.content }</span>
      <Child  item={item} />
    </div>
  )
})

export { Parent }
// Child.js
import './index.css'
import { memo } from 'react';

const Child = memo(({ item }) => {
  console.log("我是Child.js 為啥要渲染我啊");

  return (
    <div className='child'>
      我是Child.js
      <span className='item'>{ item.content }</span>
    </div>
  )
})


export { Child }

你會發現這不是跟昨天一樣吧,哈哈哈,那我按下按鈕去增加我 count 的值,一定結果跟昨天一樣,簡單拉

然後你就看著畫面,笑著笑著就哭了,心中就會產生一句話,那誒安捏?

沒錯這就是 React.memo 的罩門,來我娓娓道來

React.memo, shallowly compare?

因為React.memo 是用 shallowly compare 的方法確認 props 的值是否一樣,

shallowly compare 在 props 是 Number、String 或是 boolean 比較的是數值,當 props 是 Array、Object、Function 時,比較的是記憶體位置 (reference)。

因此,以我們的狀況為例,當父元件重新渲染時,在父元件宣告的 Object 都會被重新分配記憶體位址,所以想要利用 React.memo 防止重新渲染就會失效。

道理都我懂了,所以我該怎麼解決呢?????

其實 React.memo 是有提供第二個參數讓我們去自訂 props 的比較方式,讓我們在比較 Array、Object、Function 不再只是比較記憶體位置。

讓我們以這個例子來實踐這點吧,我們在 Parent.js 的檔案加上customCompare 這個 Function,並將他放在 React.memo 的第二個參數位置,像是這樣

// Parent.js
import './index.css'
import { Child } from "./Child"
import { memo } from 'react';


const customCompare = (pervProps, nextProps) => {
  if(pervProps.content === nextProps.content) {
    return true   
  } else {
    return false
  }
}

const Parent = memo(({ item }) => {
  console.log("我是Parent.js 為啥要渲染我啊");

  return (
    <div className='parent'>
      我是Parent.js
      <span className='item'>{ item.content }</span>
      <Child item={item} />
    </div>
  )
}, customCompare)

export { Parent }

在我們的例子中,我們將物件中的 content 拿來做比較,告訴 memo 我們要比較的規則是這樣的,而這兩個字串,
在前一次跟後一次傳遞的值都是一樣的,所以就不會渲染了

那你可能會問 Child.js 會渲染嗎?答案是不會的,因為停止渲染的動作在 Parent.js 這裡就停下來了,自然就不會往下渲染

但是這個自訂 props 的比較方式,好像也有點麻煩誒,會不會因為傳遞物件的不同,還要一直去客製化,有點累,那何不試試 useCallback

useCallback 哇跨謀?

useCallback 也是要解決一樣的問題的

也就是父層傳遞的 props 是 陣列、物件、函式 時,因為父層的狀態改變
導致重新渲染時記憶體位址也會被重新分配

而我們之前也提到了,React.memo 會用 shallowly compare 比較 props 中 陣列、物件、函式 的記憶體位址,這個比較方式會讓子元件被重新渲染。

有關 shallowly compare 可以點擊這裡看看他是如何比較的!

所以 React 提供了 useCallback 這個方法讓 React 在元件重新渲染時,如果 dependencies array 中的值在沒有被修改的情況下,他就不會渲染。

說起來很簡單,但是好像有點模糊,讓我們試試看好了!

// App.js
import { useCallback, useState } from 'react';
import './index.css';
import { Parent } from './Parent';

function App() {
  const [count, setCount] = useState(0)
  console.log("APP 組建 渲染");

  const clickHandler = () => {
    setCount(perv => perv + 1)
  }

  const objectUseCallback = useCallback(()=>{
    return { content: "傳下來的東西"}
  },[])
  
  return (
    <div className='border'>
      <div>
        我是在 APP.js 的 count :{ count }
      </div>
      <button onClick={clickHandler}>點我加一</button>
      <Parent item={objectUseCallback} />
    </div>
  );
}

export default App;

我先將宣告一個變數 objectUseCallback 而他是一個 function 回傳一個物件

const objectUseCallback = useCallback(()=>{
    return { content: "傳下來的東西"}
  },[])

值後我在 Parent.js 改寫成這樣

import './index.css'
import { Child } from "./Child"
import { memo } from 'react';

const Parent = memo(({ item }) => {
  console.log("我是Parent.js 為啥要渲染我啊");

  return (
    <div className='parent'>
      我是Parent.js
      <span className='item'>{ item().content }</span>
      <Child  item={item} />
    </div>
  )
})

export { Parent }

因為他是item變成一個 function 所以必須 item().content 才可以取出他的值

這樣之後你會發現,我的畫面還是一樣,點擊時也不會渲染子層

所以在我們的例子中,它會幫我們記住 Object,防止 Object 被重新分配記憶體位址。
這樣就可以避免父元件重新渲染後,Object 被重新分配記憶體位址,造成 React.memo 的 shallowly compare 發現傳遞的 Object 記憶體位址不同。

小結

今天說明了 useCallback 的用法其實就是要記住一個 function 而已,並且解決了子層會因為父層重複渲染的問題,這幾天會針對這些 hook 持續做說明,感謝大家觀看!


上一篇
Day 17 為什麼要渲染我啊,那你有聽過 React.memo 嗎?
下一篇
Day 19 useMemo 請問你跟 React.memo 有關係嗎?
系列文
我與 React 的 30天30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言